到目前為止,我們一直活在「瀏覽器世界」:
有 window
、有 canvas
、有 performance.now()
、有 Web Worker。
但今天如果有人在 npm 上裝了我們的套件,開在 Node 裡呼叫會發生什麼?
答案通常是:「能編譯,但跑不起來。」
這就引出一個設計哲學問題——
WASM 模組要不要支援 Node?
如果支援,要支援到什麼程度?
如果不支援,要怎麼「優雅地拒絕」?
在 Node 環境下:
fetch()
去抓 .wasm
(除非你 polyfill 或改用 fs.readFileSync
);ImageData
或 CanvasRenderingContext2D
;window.performance
或 alert
等 Web 對象;target: node
下會自動改 loader 行為,導致 .wasm?url
找不到。所以,要讓它「能跑」,你必須多包一層。
要不然就是要讓它「明確的失敗」。
若真的要支援,可以走以下策略:
使用 wasm-bindgen
的 Node 目標--target nodejs
會產生 CommonJS 封裝:
wasm-pack build --target nodejs --out-dir pkg-node
它會把 .wasm
路徑直接內嵌成 fs.readFileSync(__dirname + '/xxx_bg.wasm')
。
在 Node 中直接:
import init, { apply_pipeline } from 'img-wasm-node'
await init()
const out = apply_pipeline(bytes, w, h, ops)
輸出雙目標:pkg/
給 web,pkg-node/
給 Node。
然後在 package.json
宣告:
{
"exports": {
"import": "./pkg/img_wasm.js",
"require": "./pkg-node/img_wasm.js"
}
}
如此 Node 跑 require()
時自動導向 node 版。
額外提供 I/O 工具
沒有 canvas?可以用 sharp
或 pngjs
幫忙載圖、轉成 RGBA,再餵給 apply_pipeline()
。
import { PNG } from 'pngjs'
import fs from 'fs'
import { apply_pipeline } from 'img-wasm-node'
const png = PNG.sync.read(fs.readFileSync('input.png'))
const out = apply_pipeline(png.data, png.width, png.height, [{ kind: 'grayscale' }])
png.data = out
fs.writeFileSync('output.png', PNG.sync.write(png))
這樣整套從頭到尾就能在 Node 環境裡跑起來,適合批次影像處理或 CLI 工具。
另一派是:明確宣告「只支援瀏覽器」,並讓錯誤在第一時間爆出來,而不是模糊地掛掉。
在入口加一句保險:
#[wasm_bindgen(start)]
pub fn check_env() -> Result<(), JsValue> {
if js_sys::global().has("window") {
Ok(())
} else {
Err(js_err(ErrorCode::Unsupported, "This build only runs in browser (no window object detected)"))
}
}
這樣使用者在 Node 裡呼叫時會馬上收到一個乾淨的錯誤:
{ "code": "UNSUPPORTED", "message": "This build only runs in browser (no window object detected)" }
而不是一堆奇怪的「ReferenceError: window is not defined」。
fs
版初始化邏輯與 pngjs
範例)最怕的是第三種:死的不明不白
如果你能跑,就讓它跑得順;
如果不能跑,就讓它死得乾脆。